package com.lurencun.android.support.v2.widget;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.ImageView;

public class TouchableImageView extends ImageView {

	public interface OnBitmapChangedListener {

		void onBitmapChanged(Bitmap bitmap);
	};

	protected enum Command {
		Center, Move, Zoom, Layout, Reset,
	};

	public static final String LOG_TAG = "image";

	protected Matrix mBaseMatrix = new Matrix();
	protected Matrix mSuppMatrix = new Matrix();
	protected Handler mHandler = new Handler();
	protected Runnable mOnLayoutRunnable = null;
	protected float mMaxZoom;
	protected final Matrix mDisplayMatrix = new Matrix();
	protected final float[] mMatrixValues = new float[9];
	protected int mThisWidth = -1, mThisHeight = -1;

	final protected RotateBitmap mBitmapDisplayed = new RotateBitmap(null, 0);
	final protected float MAX_ZOOM = 2.0f;

	private OnBitmapChangedListener mListener;

	public TouchableImageView(Context context) {
		super(context);
		init();
	}

	public TouchableImageView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	public void setOnBitmapChangedListener(OnBitmapChangedListener listener) {
		mListener = listener;
	}

	protected void init() {
		setScaleType(ImageView.ScaleType.MATRIX);
	}

	public void clear() {
		setImageBitmapReset(null, true);
	}

	@Override
	protected void onLayout(boolean changed, int left, int top, int right,
			int bottom) {
		super.onLayout(changed, left, top, right, bottom);
		mThisWidth = right - left;
		mThisHeight = bottom - top;
		Runnable r = mOnLayoutRunnable;
		if (r != null) {
			mOnLayoutRunnable = null;
			r.run();
		}
		if (mBitmapDisplayed.getBitmap() != null) {
			getProperBaseMatrix(mBitmapDisplayed, mBaseMatrix);
			setImageMatrix(Command.Layout, getImageViewMatrix());
		}
	}

	public void setImageBitmapReset(final Bitmap bitmap, final boolean reset) {
		setImageRotateBitmapReset(new RotateBitmap(bitmap, 0), reset);
	}

	public void setImageBitmapReset(final Bitmap bitmap, final int rotation,
			final boolean reset) {
		setImageRotateBitmapReset(new RotateBitmap(bitmap, rotation), reset);
	}

	public void setImageRotateBitmapReset(final RotateBitmap bitmap,
			final boolean reset) {
		Log.d(LOG_TAG, "setImageRotateBitmapReset");

		final int viewWidth = getWidth();
		if (viewWidth <= 0) {
			mOnLayoutRunnable = new Runnable() {

				public void run() {
					setImageBitmapReset(bitmap.getBitmap(),
							bitmap.getRotation(), reset);
				}
			};
			return;
		}

		if (bitmap.getBitmap() != null) {
			getProperBaseMatrix(bitmap, mBaseMatrix);
			setImageBitmap(bitmap.getBitmap(), bitmap.getRotation());
		} else {
			mBaseMatrix.reset();
			setImageBitmap(null);
		}

		if (reset) {
			mSuppMatrix.reset();
		}

		setImageMatrix(Command.Reset, getImageViewMatrix());
		mMaxZoom = maxZoom();

		if (mListener != null) {
			mListener.onBitmapChanged(bitmap.getBitmap());
		}
	}

	protected float maxZoom() {
		if (mBitmapDisplayed.getBitmap() == null) {
			return 1F;
		}
		float fw = (float) mBitmapDisplayed.getWidth() / (float) mThisWidth;
		float fh = (float) mBitmapDisplayed.getHeight() / (float) mThisHeight;
		float max = Math.max(fw, fh) * 4;
		return max;
	}

	public RotateBitmap getDisplayBitmap() {
		return mBitmapDisplayed;
	}

	public float getMaxZoom() {
		return mMaxZoom;
	}

	@Override
	public void setImageBitmap(Bitmap bitmap) {
		setImageBitmap(bitmap, 0);
	}

	/**
	 * This is the ultimate method called when a new bitmap is set
	 * 
	 * @param bitmap
	 * @param rotation
	 */
	protected void setImageBitmap(Bitmap bitmap, int rotation) {
		super.setImageBitmap(bitmap);
		Drawable d = getDrawable();
		if (d != null) {
			d.setDither(true);
		}
		mBitmapDisplayed.setBitmap(bitmap);
		mBitmapDisplayed.setRotation(rotation);
	}

	protected Matrix getImageViewMatrix() {
		mDisplayMatrix.set(mBaseMatrix);
		mDisplayMatrix.postConcat(mSuppMatrix);
		return mDisplayMatrix;
	}

	/**
	 * Setup the base matrix so that the image is centered and scaled properly.
	 * 
	 * @param bitmap
	 * @param matrix
	 */
	protected void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix) {
		float viewWidth = getWidth();
		float viewHeight = getHeight();
		float w = bitmap.getWidth();
		float h = bitmap.getHeight();
		matrix.reset();
		float widthScale = Math.min(viewWidth / w, MAX_ZOOM);
		float heightScale = Math.min(viewHeight / h, MAX_ZOOM);
		float scale = Math.min(widthScale, heightScale);
		matrix.postConcat(bitmap.getRotateMatrix());
		matrix.postScale(scale, scale);
		matrix.postTranslate((viewWidth - w * scale) / MAX_ZOOM,
				(viewHeight - h * scale) / MAX_ZOOM);
	}

	protected float getValue(Matrix matrix, int whichValue) {
		matrix.getValues(mMatrixValues);
		return mMatrixValues[whichValue];
	}

	protected RectF getBitmapRect() {
		if (mBitmapDisplayed.getBitmap() == null)
			return null;
		Matrix m = getImageViewMatrix();
		RectF rect = new RectF(0, 0, mBitmapDisplayed.getBitmap().getWidth(),
				mBitmapDisplayed.getBitmap().getHeight());
		m.mapRect(rect);
		return rect;
	}

	protected float getScale(Matrix matrix) {
		return getValue(matrix, Matrix.MSCALE_X);
	}

	public float getScale() {
		return getScale(mSuppMatrix);
	}

	protected void center(boolean horizontal, boolean vertical) {
		if (mBitmapDisplayed.getBitmap() == null)
			return;
		RectF rect = getCenter(horizontal, vertical);
		if (rect.left != 0 || rect.top != 0) {
			postTranslate(rect.left, rect.top);
		}
	}

	protected void setImageMatrix(Command command, Matrix matrix) {
		setImageMatrix(matrix);
	}

	protected RectF getCenter(boolean horizontal, boolean vertical) {
		if (mBitmapDisplayed.getBitmap() == null)
			return new RectF(0, 0, 0, 0);
		RectF rect = getBitmapRect();
		float height = rect.height();
		float width = rect.width();
		float deltaX = 0, deltaY = 0;
		if (vertical) {
			int viewHeight = getHeight();
			if (height < viewHeight) {
				deltaY = (viewHeight - height) / 2 - rect.top;
			} else if (rect.top > 0) {
				deltaY = -rect.top;
			} else if (rect.bottom < viewHeight) {
				deltaY = getHeight() - rect.bottom;
			}
		}
		if (horizontal) {
			int viewWidth = getWidth();
			if (width < viewWidth) {
				deltaX = (viewWidth - width) / 2 - rect.left;
			} else if (rect.left > 0) {
				deltaX = -rect.left;
			} else if (rect.right < viewWidth) {
				deltaX = viewWidth - rect.right;
			}
		}
		return new RectF(deltaX, deltaY, 0, 0);
	}

	protected void postTranslate(float deltaX, float deltaY) {
		mSuppMatrix.postTranslate(deltaX, deltaY);
		setImageMatrix(Command.Move, getImageViewMatrix());
	}

	protected void postScale(float scale, float centerX, float centerY) {
		mSuppMatrix.postScale(scale, scale, centerX, centerY);
		setImageMatrix(Command.Zoom, getImageViewMatrix());
	}

	protected void zoomTo(float scale) {
		float cx = getWidth() / 2F;
		float cy = getHeight() / 2F;
		zoomTo(scale, cx, cy);
	}

	public void zoomTo(float scale, float durationMs) {
		float cx = getWidth() / 2F;
		float cy = getHeight() / 2F;
		zoomTo(scale, cx, cy, durationMs);
	}

	protected void zoomTo(float scale, float centerX, float centerY) {
		if (scale > mMaxZoom)
			scale = mMaxZoom;
		float oldScale = getScale();
		float deltaScale = scale / oldScale;
		postScale(deltaScale, centerX, centerY);
		onZoom(getScale());
		center(true, true);
	}

	protected void onZoom(float scale) {
	}

	public void scrollBy(float x, float y) {
		panBy(x, y);
	}

	protected void panBy(float dx, float dy) {
		RectF rect = getBitmapRect();
		RectF srect = new RectF(dx, dy, 0, 0);
		updateRect(rect, srect);
		postTranslate(srect.left, srect.top);
		center(true, true);
	}

	protected void updateRect(RectF bitmapRect, RectF scrollRect) {
		float width = getWidth();
		float height = getHeight();

		if (bitmapRect.top >= 0 && bitmapRect.bottom <= height)
			scrollRect.top = 0;
		if (bitmapRect.left >= 0 && bitmapRect.right <= width)
			scrollRect.left = 0;
		if (bitmapRect.top + scrollRect.top >= 0 && bitmapRect.bottom > height)
			scrollRect.top = (int) (0 - bitmapRect.top);
		if (bitmapRect.bottom + scrollRect.top <= (height - 0)
				&& bitmapRect.top < 0)
			scrollRect.top = (int) ((height - 0) - bitmapRect.bottom);
		if (bitmapRect.left + scrollRect.left >= 0)
			scrollRect.left = (int) (0 - bitmapRect.left);
		if (bitmapRect.right + scrollRect.left <= (width - 0))
			scrollRect.left = (int) ((width - 0) - bitmapRect.right);
		// Log.d( LOG_TAG, "scrollRect(2): " + scrollRect.toString() );
	}

	protected void scrollBy(float distanceX, float distanceY,
			final float durationMs) {
		final float dx = distanceX;
		final float dy = distanceY;
		final long startTime = System.currentTimeMillis();
		mHandler.post(new Runnable() {

			float old_x = 0;
			float old_y = 0;

			public void run() {
				long now = System.currentTimeMillis();
				float currentMs = Math.min(durationMs, now - startTime);
				float x = easeOut(currentMs, 0, dx, durationMs);
				float y = easeOut(currentMs, 0, dy, durationMs);
				panBy((x - old_x), (y - old_y));
				old_x = x;
				old_y = y;
				if (currentMs < durationMs) {
					mHandler.post(this);
				} else {
					RectF centerRect = getCenter(true, true);
					if (centerRect.left != 0 || centerRect.top != 0)
						scrollBy(centerRect.left, centerRect.top);
				}
			}
		});
	}

	private float easeOut(float time, float start, float end, float duration) {
		return end * ((time = time / duration - 1) * time * time + 1) + start;
	}

	protected void zoomTo(float scale, final float centerX,
			final float centerY, final float durationMs) {
		// Log.d( LOG_TAG, "zoomTo: " + scale + ", " + centerX + ": " + centerY
		// );
		final long startTime = System.currentTimeMillis();
		final float incrementPerMs = (scale - getScale()) / durationMs;
		final float oldScale = getScale();
		mHandler.post(new Runnable() {

			public void run() {
				long now = System.currentTimeMillis();
				float currentMs = Math.min(durationMs, now - startTime);
				float target = oldScale + (incrementPerMs * currentMs);
				zoomTo(target, centerX, centerY);
				if (currentMs < durationMs) {
					mHandler.post(this);
				} else {
					// if ( getScale() < 1f ) {}
				}
			}
		});
	}

	public void dispose() {
		if (mBitmapDisplayed.getBitmap() != null) {
			if (!mBitmapDisplayed.getBitmap().isRecycled()) {
				mBitmapDisplayed.getBitmap().recycle();
			}
		}
		clear();
	}
}
